///////////////////////////////////////////////////////////////////////////////
//
//  Copyright
//
///////////////////////////////////////////////////////////////////////////////

/**
 * @author					Dimarik
 * @version					1.0
 * @playerversion			Flash 9
 * @langversion				3.0
 *
 * @keyword					gifdecoder, bitmap
 */
package ru.goodcore.utils.gif
{

import flash.display.BitmapData;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.utils.ByteArray;

import ru.goodcore.utils.traceByteArray;

public class GIFDecoder_ {

	//-------------------------------------------------------------------------
	//
	//  Constructor
	//
	//-------------------------------------------------------------------------

	/**
	 * @private
	 * File read status: No errors.
	 */
	private static var STATUS_OK:				int = 0;

	/**
	 * @private
	 * File read status: Error decoding file (may be partially decoded)
	 */
	private static var STATUS_FORMAT_ERROR:		int = 1;

	/**
	 * @private
	 * File read status: Unable to open source.
	 */
	private static var STATUS_OPEN_ERROR:		int = 2;

	/**
	 * @private
	 */
	private static var frameRect:				Rectangle = new Rectangle();

	//-------------------------------------------------------------------------
	//
	//  Constructor
	//
	//-------------------------------------------------------------------------

	/**
	 * Constructor
	 */
	public function GIFDecoder() {
		this.block = new ByteArray();
	}

	//-------------------------------------------------------------------------
	//
	//  Constructor
	//
	//-------------------------------------------------------------------------

	/**
	 * @private 
	 */
	private var sheet:					BitmapData;

	/**
	 * @private 
	 */
	private var inStream:				ByteArray;

	/**
	 * @private 
	 */
	private var status:					int;

	/**
	 * @private
	 * full image width 
	 */
	private var width:					int;

	/**
	 * @private
	 * full image height
	 */
	private var height:					int;

	/**
	 * @private
	 * global color table used
	 */
	private var gctFlag:				Boolean = false;

	/**
	 * @private
	 * size of global color table
	 */
	private var gctSize:				int;

	/**
	 * @private
	 * iterations; 0 = repeat forever
	 */
	private var loopCount:				int = 1;

	/**
	 * @private
	 * global color table
	 */
	private var gct:					Array;

	/**
	 * @private
	 * local color table
	 */
	private var lct:					Array;

	/**
	 * @private
	 * active color table
	 */
	private var act:					Array;

	/**
	 * @private
	 * background color index
	 */
	private var bgIndex:				int;

	/**
	 * @private
	 * background color
	 */
	private var bgColor:				int;

	/**
	 * @private
	 * previous bg color
	 */
	private var lastBgColor:			int;

	/**
	 * @private
	 * pixel aspect ratio
	 */
	private var pixelAspect:			int;

	/**
	 * @private
	 * local color table flag
	 */
	private var lctFlag:				Boolean = false;

	/**
	 * @private
	 * interlace flag
	 */
	private var interlace:				Boolean;

	/**
	 * @private
	 * local color table size
	 */
	private var lctSize:				int;

	/**
	 * @private
	 */
	private var ix:						int;

	/**
	 * @private
	 */
	private var iy:						int;

	/**
	 * @private
	 * 
	 */
	private var iw:						int;

	/**
	 * @private
	 * current image rectangle
	 */
	private var ih:						int;

	/**
	 * @private
	 * last image rect
	 */
	private var lastRect:				Rectangle;

	/**
	 * @private
	 * current frame
	 */
	private var image:					BitmapData;

	/**
	 * @private
	 */
	private var bitmap:					BitmapData;

	/**
	 * @private
	 * previous frame
	 */
	private var lastImage:				BitmapData;

	/**
	 * @private
	 * current data block
	 */
	private var block:					ByteArray;

	/**
	 * @private
	 * block size
	 */
	private var blockSize:				int = 0;

	/**
	 * @private
	 * last graphic control extension info
	 */
	private var dispose:				int= 0;

	/**
	 * @private
	 * 0 = no action; 
	 * 1 = leave in place;
	 * 2 = restore to bg;
	 * 3 = restore to prev
	 */
	private var lastDispose:			int = 0;

	/**
	 * @private
	 * use transparent color
	 */
	private var transparency:			Boolean = false;

	/**
	 * @private
	 * delay in milliseconds
	 */
	private var delay:					int = 0;

	/**
	 * @private
	 * transparent color index
	 */
	private var transIndex:				int;

	/**
	 * @private
	 * max decoder pixel stack size
	 */
	private static var MaxStackSize:	int = 4096;

	/**
	 * @private
	 * LZW decoder working arrays
	 */
	private var prefix:					Array

	/**
	 * @private
	 */
	private var suffix:					Array;

	/**
	 * @private
	 */
	private var pixelStack:				Array;

	/**
	 * @private
	 */
	private var pixels:					Array;

	/**
	 * @private
	 * frames read from current file
	 */
	private var frames:					Array;

	/**
	 * @private
	 */
	private var frameCount:				int;






	//-------------------------------------------------------------------------
	//
	//  Public methods
	//
	//-------------------------------------------------------------------------

	public function decode(value:ByteArray):BitmapData {
		this.read(value);
		
		//trace(traceByteArray(value));

		var len:	uint = this.frames.length;
		var frame:	GIFFrame = GIFFrame(frames[i]);
		var width:	int = frame.bitmapData.width;
		var height:	int = frame.bitmapData.height;
		var point:	Point = new Point();

		this.sheet = new BitmapData( width * len, height, frame.bitmapData.transparent);

		this.sheet.lock();

		for (var i:uint = 0; i < len; i++) {
			frame = GIFFrame(frames[i]);
			//trace(traceByteArray(frame.bitmapData.getPixels(this.sheet.rect)))
			this.sheet.copyPixels(frame.bitmapData, frame.bitmapData.rect, point);
			point.x += width;
		}

		this.sheet.unlock();

		return this.sheet;
	}

	public function get disposeValue():int {
		return this.dispose;
	}
		
	/**
	 * Gets display duration for specified frame.
	 *
	 * @param n int index of frame
	 * @return delay in milliseconds
	 */
	public function getDelay(frame:int):int {
		this.delay = -1;
		if (frame >= 0 && frame < this.frameCount) {
			this.delay = this.frames[frame].delay;
		}
		return this.delay;
	}

	/**
	 * Gets the number of frames read from file.
	 * @return frame count
	 */
	public function getFrameCount():int
	{
		return frameCount;
	}

	/**
	 * Gets the first (or only) image read.
	 *
	 * @return BitmapData containing first frame, or null if none.
	 */
	public function getImage():GIFFrame {
		return this.getFrame(0);
	}

	/**
	 * Gets the "Netscape" iteration count, if any.
	 * A count of 0 means repeat indefinitiely.
	 *
	 * @return iteration count if one was specified, else 1.
	 */
	public function getLoopCount():int {
		return this.loopCount;
	}

	/**
	 * Gets image size.
	 *
	 * @return GIF image dimensions
	 */
	public function getFrameSize():Rectangle {
		var rect:Rectangle = GIFDecoder.frameRect;
		rect.x = 
		rect.y = 0;

		rect.width = width;
		rect.height = height;
		return rect;
	}

	/**
	 * Reads GIF image from stream
	 *
	 * @param BufferedInputStream containing GIF file.
	 * @return read status code (0 = no errors)
	 */
	public function read( inStream:ByteArray ):int {
		init();
		if ( inStream != null ) {
			this.inStream = inStream;
			readHeader();

			if ( !hasError() ) {
				readContents();
				if (frameCount < 0) status = STATUS_FORMAT_ERROR;
			}
		} 
		else {
			status = STATUS_OPEN_ERROR;
		}
		return status;
	}

	/**
	 * Gets the image contents of frame n.
	 *
	 * @return BufferedImage representation of frame, or null if n is invalid.
	 */
	public function getFrame(n:int):GIFFrame {
		var im:GIFFrame = null;
		if (n >= 0 && n < frameCount)
			im = frames[n];
		else throw new RangeError ("Wrong frame number passed");

		return im;
	}

	//-------------------------------------------------------------------------
	//
	//  Private methods
	//
	//-------------------------------------------------------------------------

	/**
	 * @private
	 * Creates new frame image from current data (and previous
	 * frames as specified by their disposition codes).
	 */
	private function getPixels(bitmap:BitmapData):Array {	
		var pixels:Array = new Array ( 4 * image.width * image.height );
		var count:int = 0;
		var lngWidth:int = image.width;
		var lngHeight:int = image.height;
		var color:int;

		for (var th:int = 0; th < lngHeight; th++) {
			for (var tw:int = 0; tw < lngWidth; tw++) {
				color = bitmap.getPixel (th, tw);
				pixels[count++] = (color & 0xFF0000) >> 16;
				pixels[count++] = (color & 0x00FF00) >> 8;
				pixels[count++] = (color & 0x0000FF);
			}
		}
		return pixels;
	}

	/**
	 * @private 
	 * @param pixels
	 */
	private function setPixels(pixels:Array):void {
		var count:int = 0;
		var color:int;
		pixels.position = 0;
		
		var lngWidth:int = this.image.width;
		var lngHeight:int = this.image.height;
		this.bitmap.lock();
		
		for (var th:int = 0; th < lngHeight; th++) {
			for (var tw:int = 0; tw < lngWidth; tw++) {
				color = this.pixels[int(count++)];
				this.bitmap.setPixel32 ( tw, th, color );
			}
		}
		this.bitmap.unlock();
	}

	/**
	 * @private 
	 */
	private function transferPixels():void {
		// expose destination image's pixels as int array
		var dest:Array = getPixels( this.image );
		// fill in starting image contents based on last image's dispose code
		if (lastDispose > 0) {
			if (lastDispose == 3) {
				// use image before last
				var n:int = frameCount - 2;
				lastImage = n > 0 ? getFrame(n - 1).bitmapData : null;
				
			}

			if (lastImage != null) {
				var prev:Array = getPixels( lastImage );	
				dest = prev.slice();
				// copy pixels
				if (lastDispose == 2) {
					// fill last image rect area with background color
					var c:Number;
					 // assume background is transparent
					c = transparency ? 0x00000000 : lastBgColor;
					// use given background color
					image.fillRect( lastRect, c );
				}
			}
		}

		// copy each source line to the appropriate place in the destination
		var pass:int = 1;
		var inc:int = 8;
		var iline:int = 0;
		for (var i:int = 0; i < ih; i++) {
			var line:int = i;
			if (interlace) {
				if (iline >= ih) {
					pass++;
					switch (pass) {
						case 2 :
							iline = 4;
							break;
						case 3 :
							iline = 2;
							inc = 4;
							break;
						case 4 :
							iline = 1;
							inc = 2;
							break;
					}
				}
				line = iline;
				iline += inc;
			}
			line += iy;
			if (line < height) {
				var k:int = line * width;
				var dx:int = k + ix; // start of line in dest
				var dlim:int = dx + iw; // end of dest line
				if ((k + width) < dlim) {
					dlim = k + width; // past dest edge
				}
				var sx:int = i * iw; // start of line in source
				var index:int;
				var tmp:int;
				while (dx < dlim) {
					// map color and insert in destination
					index = (pixels[sx++]) & 0xff;
					tmp = act[index];
					if (tmp != 0)
						dest[dx] = tmp;
					dx++;
				}
			}
		}
		setPixels( dest );
	}



	/**
	 * @private
	 * Decodes LZW image data into pixel array.
	 * Adapted from John Cristy's ImageMagick.
	 */
	private function decodeImageData():void {
		var NullCode:int = -1;
		var npix:int = iw * ih;
		var available:int;
		var clear:int;
		var code_mask:int;
		var code_size:int;
		var end_of_information:int;
		var in_code:int;
		var old_code:int;
		var bits:int;
		var code:int;
		var count:int;
		var i:int;
		var datum:int;
		var data_size:int;
		var first:int;
		var top:int;
		var bi:int;
		var pi:int;

		if (pixels == null || pixels.length < npix) {
			pixels = new Array ( npix ); // allocate new pixel array
		}
		if (prefix == null) prefix = new Array ( MaxStackSize );
		if (suffix == null) suffix = new Array ( MaxStackSize );
		if (pixelStack == null) pixelStack = new Array ( MaxStackSize + 1 );

		//  Initialize GIF data stream decoder.

		data_size = readSingleByte();
		clear = 1 << data_size;
		end_of_information = clear + 1;
		available = clear + 2;
		old_code = NullCode;
		code_size = data_size + 1;
		code_mask = (1 << code_size) - 1;

		for (code = 0; code < clear; code++) {
			prefix[int(code)] = 0;
			suffix[int(code)] = code;
		}

		//  Decode GIF pixel stream.
		datum = bits = count = first = top = pi = bi = 0;

		for (i = 0; i < npix;) {
			if (top == 0) {
				if (bits < code_size) {
					//  Load bytes until there are enough bits for a code.
					if (count == 0) {
						// Read a new data block.
						count = readBlock();
						if (count <= 0)
							break;
						bi = 0;
					}
					datum += (int((block[int(bi)])) & 0xff) << bits;
					bits += 8;
					bi++;
					count--;
					continue;
				}

				//  Get the next code.
				code = datum & code_mask;
				datum >>= code_size;
				bits -= code_size;
				//  Interpret the code
				if (code > available || code == end_of_information)
					break;

				if (code == clear) {
					//  Reset decoder.
					code_size = data_size + 1;
					code_mask = (1 << code_size) - 1;
					available = clear + 2;
					old_code = NullCode;
					continue;
				}

				if (old_code == NullCode) {
					pixelStack[int(top++)] = suffix[int(code)];
					old_code = code;
					first = code;
					continue;
				}

				in_code = code;

				if (code == available) {
					pixelStack[int(top++)] = first;
					code = old_code;
				}

				while (code > clear) {
					pixelStack[int(top++)] = suffix[int(code)];
					code = prefix[int(code)];
				}

				first = (suffix[int(code)]) & 0xff;

				//  Add a new string to the string table,
				if (available >= MaxStackSize) break;
				pixelStack[int(top++)] = first;
				prefix[int(available)] = old_code;
				suffix[int(available)] = first;
				available++;

				if (
					((available & code_mask) == 0) &&
					available < MaxStackSize
				) {
					code_size++;
					code_mask += available;
				}
				old_code = in_code;
			}

			//  Pop a pixel off the pixel stack.
			top--;
			pixels[int(pi++)] = pixelStack[int(top)];
			i++;
		}

		for (i = pi; i < npix; i++) {
			pixels[int(i)] = 0; // clear missing pixels
		}

	}

	/**
	 * @private
	 * Returns true if an error was encountered during reading/decoding
	 */
	private function hasError():Boolean {
		return status != STATUS_OK;
	}

	/**
	 * @private
	 * Initializes or re-initializes reader
	 */
	private function init():void {
		status = STATUS_OK;
		frameCount = 0;
		frames = new Array;
		gct = null;
		lct = null;
	}

	/**
	 * @private
	 * Reads a single byte from the input stream.
	 */
	private function readSingleByte():int {
		var curByte:int = 0;
		try {
			curByte = inStream.readUnsignedByte();
		} 
		catch (e:Error) {
			status = STATUS_FORMAT_ERROR;
		}

		return curByte;
	}

	/**
	 * @private
	 * Reads next variable length block from input.
	 *
	 * @return number of bytes stored in "buffer"
	 */
	private function readBlock():int {
		blockSize = readSingleByte();
		var n:int = 0;
		if (blockSize > 0) {
			try {
				var count:int = 0;

				while (n < blockSize) {
					inStream.readBytes(block, n, blockSize - n);
					if ( (blockSize - n) == -1) 
						break;
					n += (blockSize - n);
				}
			} 
			catch (e:Error)	{}

			if (n < blockSize)
				status = STATUS_FORMAT_ERROR;
		}
		return n;
	}

	/**
	 * @private
	 * Reads color table as 256 RGB integer values
	 *
	 * @param ncolors int number of colors to read
	 * @return int array containing 256 colors (packed ARGB with full alpha)
	 */
	private function readColorTable(ncolors:int):Array 
	{
		var nbytes:int = 3 * ncolors;
		var tab:Array = null;
		var c:ByteArray = new ByteArray;
		var n:int = 0;
		try {
			inStream.readBytes(c, 0, nbytes );
			n = nbytes;
		} 
		catch (e:Error) {}

		if (n < nbytes)
			status = STATUS_FORMAT_ERROR;
		else {
			tab = new Array(256); // max size to avoid bounds checks
			var i:int = 0;
			var j:int = 0;
			while (i < ncolors) {
				var r:int = (c[j++]) & 0xff;
				var g:int = (c[j++]) & 0xff;
				var b:int = (c[j++]) & 0xff;
				tab[i++] = ( 0xff000000 | (r << 16) | (g << 8) | b );
			}
		}
		return tab;
	}

	/**
	 * @private
	 * Main file parser.  Reads GIF content blocks.
	 */
	private function readContents():void {
		// read GIF file content blocks
		var done:Boolean = false;

		while (!(done || this.hasError())) {
			
			var code:int = readSingleByte();
			
			switch (code) {

				case 0x2C : // image separator
					readImage();
					break;

				case 0x21 : // extension
					code = readSingleByte();

					switch (code) {
						case 0xf9 : // graphics control extension
							readGraphicControlExt();
							break;
	
						case 0xff : // application extension
							readBlock();
							var app:String = "";
							for (var i:int = 0; i < 11; i++) 
							{
								app += block[int(i)];
							}
							if (app == "NETSCAPE2.0") 
							{
								readNetscapeExt();
							}
							else
								skip(); // don't care
							break;
	
						default : // uninteresting extension
							skip();
							break;
					}
					break;

				case 0x3b : // terminator
					done = true;
					break;

				case 0x00 : // bad byte, but keep going and see what happens
					break;

				default :
					status = STATUS_FORMAT_ERROR;
					break;
			}
		}
	}

	/**
	 * @private
	 * Reads Graphics Control Extension values
	 */
	private function readGraphicControlExt():void {
		readSingleByte(); // block size
		var packed:int = readSingleByte(); // packed fields
		dispose = (packed & 0x1c) >> 2; // disposal method
		if (dispose == 0) 
		{
			dispose = 1; // elect to keep old image if discretionary
		}
		transparency = (packed & 1) != 0;
		delay = readShort() * 10; // delay in milliseconds
		transIndex = readSingleByte(); // transparent color index
		readSingleByte(); // block terminator
	}

	/**
	 * @private
	 * Reads GIF file header information.
	 */
	private function readHeader():void {
		var id:String = "";
		for (var i:int = 0; i < 6; i++) {
			id += String.fromCharCode (readSingleByte());

		}

		if ( id.indexOf("GIF") != 0 ) {
			status = STATUS_FORMAT_ERROR;
			throw new FileTypeError ( "Invalid file type" );
			return;
		}

		this.readLSD();

		if (gctFlag && !hasError()) {
			gct = readColorTable(gctSize);
			bgColor = gct[bgIndex];
		}
	}

	/**
	 * @private
	 * Reads next frame image
	 */
	private function readImage():void {
		ix = readShort(); // (sub)image position & size
		iy = readShort();
		iw = readShort();
		ih = readShort();

		var packed:int = readSingleByte();
		lctFlag = (packed & 0x80) != 0; // 1 - local color table flag
		interlace = (packed & 0x40) != 0; // 2 - interlace flag
		// 3 - sort flag
		// 4-5 - reserved
		lctSize = 2 << (packed & 7); // 6-8 - local color table size

		if (lctFlag) 
		{
			lct = readColorTable(lctSize); // read table
			act = lct; // make local table active
		} 
		else 
		{
			act = gct; // make global table active
			if (bgIndex == transIndex)
				bgColor = 0;
		}
		var save:int = 0;
		if (transparency) 
		{
			save = act[transIndex];
			act[transIndex] = 0; // set transparent color if specified
		}

		if (act == null) 
		{
			status = STATUS_FORMAT_ERROR; // no color table defined
		}

		if (hasError()) return;

		decodeImageData(); // decode pixel data
		skip();
		if (hasError()) return;

		frameCount++;
		// create new image to receive frame data

		this.bitmap = new BitmapData ( width, height );

		this.image = this.bitmap;

		transferPixels(); // transfer pixel data to image
		trace('=============')
		trace(    traceByteArray( this.bitmap.getPixels(this.bitmap.rect) )      );

		frames.push ( new GIFFrame (bitmap, delay) ); // add image to frame list

		if (transparency) act[transIndex] = save;
		
		resetFrame();

	}

	/**
	 * @private
	 * Reads Logical Screen Descriptor
	 */
	private function readLSD():void {
		// logical screen size
		width = readShort();
		height = readShort();

		// packed fields
		var packed:int = readSingleByte();

		gctFlag = (packed & 0x80) != 0; // 1   : global color table flag
		// 2-4 : color resolution
		// 5   : gct sort flag
		gctSize = 2 << (packed & 7); // 6-8 : gct size
		bgIndex = readSingleByte(); // background color index
		pixelAspect = readSingleByte(); // pixel aspect ratio
	}

	/**
	 * @private
	 * Reads Netscape extenstion to obtain iteration count
	 */
	private function readNetscapeExt():void {
		do {
			readBlock();
			if (block[0] == 1) {
				// loop count sub-block
				var b1:int = (block[1]) & 0xff;
				var b2:int = (block[2]) & 0xff;
				loopCount = (b2 << 8) | b1;
			}
		} while ( blockSize > 0 && !this.hasError() );
	}

	/**
	 * @private
	 * Reads next 16-bit value, LSB first
	 */
	private function readShort():int {
		return this.readSingleByte() | (this.readSingleByte() << 8);
	}

	/**
	 * @private
	 * Resets frame state for reading next image.
	 */
	private function resetFrame():void {
		this.lastDispose = dispose;
		this.lastRect = new Rectangle(ix, iy, iw, ih);
		this.lastImage = image;
		this.lastBgColor = bgColor;
		// int dispose = 0;
		var transparency:Boolean = false;
		var delay:int = 0;
		this.lct = null;
	}

	/**
	 * @private
	 * Skips variable length blocks up to and including
	 * next zero length block.
	 */
	private function skip():void {
		do {
			this.readBlock();
		} while ( this.blockSize > 0 && !this.hasError() );
	}

}
}